#! /usr/bin/env python2

# Copyright (c) 2004-2005, 2007 The Regents of The University of Michigan
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Authors: Steve Reinhardt
#          Ali Saidi

# Important!
# This script expects a simple $ prompt, if you are using a shell other than
# sh which defaults to this you'll need to add something like the following
# to your bashrc/bash_profile script:
#if [ "$OAR_USER" = "xxxx" ]; then
#   PS1='$ '


import sys
import os
import re
import time
import optparse

import pexpect

progname = os.path.basename(sys.argv[0])

usage = "%prog [options] command [command arguments]"
optparser = optparse.OptionParser(usage=usage)
optparser.allow_interspersed_args=False
optparser.add_option('-e', dest='stderr_file',
                     help='command stderr output file')
optparser.add_option('-o', dest='stdout_file',
                     help='command stdout output file')
optparser.add_option('-l', dest='save_log', action='store_true',
                     help='save oarsub output log file')
optparser.add_option('-N', dest='job_name',
                     help='oarsub job name')
optparser.add_option('-q', dest='dest_queue',
                     help='oarsub destination queue')
optparser.add_option('--qwait', dest='oarsub_timeout', type='int',
                     help='oarsub queue wait timeout', default=30*60)
optparser.add_option('-t', dest='cmd_timeout', type='int',
                     help='command execution timeout', default=600*60)

(options, cmd) = optparser.parse_args()

if cmd == []:
    print >>sys.stderr, "%s: missing command" % progname
    sys.exit(1)

# If we want to do this, need to add check here to make sure cmd[0] is
# a valid PBS job name, else oarsub will die on us.
#
#if not options.job_name:
#    options.job_name = cmd[0]

cwd = os.getcwd()

# Deal with systems where /n is a symlink to /.automount
if cwd.startswith('/.automount/'):
    cwd = cwd.replace('/.automount/', '/n/', 1)

if not cwd.startswith('/n/poolfs/'):
    print >>sys.stderr, "Error: current directory must be under /n/poolfs."
    sys.exit(1)

# The Shell class wraps pexpect.spawn with some handy functions that
# assume the thing on the other end is a Bourne/bash shell.
class Shell(pexpect.spawn):
    # Regexp to match the shell prompt.  We change the prompt to
    # something fixed and distinctive to make it easier to match
    # reliably.
    prompt_re = re.compile('qdo\$ ')

    def __init__(self, cmd):
        # initialize base pexpect.spawn object
        try:
            pexpect.spawn.__init__(self, cmd)
        except pexpect.ExceptionPexpect, exc:
            print "%s:" % progname, exc
            sys.exit(1)
        # full_output accumulates the full output of the session
        self.full_output = ""
        self.quick_timeout = 15
        # wait for a prompt, then change it
        try:
            self.expect('\$ ', options.oarsub_timeout)
        except pexpect.TIMEOUT:
            print >>sys.stderr, "%s: oarsub timed out." % progname
            self.kill(9)
            self.safe_close()
            sys.exit(1)
        self.do_command('unset PROMPT_COMMAND; PS1="qdo$ "')

    # version of expect that updates full_output too
    def expect(self, regexp, timeout = -1):
        pexpect.spawn.expect(self, regexp, timeout)
        self.full_output += self.before + self.after

    # Just issue a command and wait for the next prompt.
    # Returns a string containing the output of the command.
    def do_bare_command(self, cmd, timeout = -1):
        global full_output
        self.sendline(cmd)
        # read back the echo of the command
        self.readline()
        # wait for the next prompt
        self.expect(self.prompt_re, timeout)
        output = self.before.rstrip()
        return output

    # Issue a command, then query its exit status.
    # Returns a (string, int) tuple with the command output and the status.
    def do_command(self, cmd, timeout = -1):
        # do the command itself
        output = self.do_bare_command(cmd, timeout)
        # collect status
        status = int(self.do_bare_command("echo $?", self.quick_timeout))
        return (output, status)

    # Check to see if the given directory exists.
    def dir_exists(self, dirname):
        (output, status) = shell.do_command('[ -d %s ]' % dirname,
                                            self.quick_timeout)
        return status == 0

    # Don't actually try to close it.. just wait until it closes by itself
    # We can't actually kill the pid which is what it's trying to do, and if
    # we call wait we could be in an unfortunate situation of it printing input
    # right as we call wait, so the input is never read and the process never ends
    def safe_close(self):
        count = 0
        while self.isalive() and count < 10:
            time.sleep(1)
        self.close(force=False)

# Spawn the interactive pool job.

# Hack to do link on poolfs... disabled for now since
# compiler/linker/library versioning problems between poolfs and
# nodes.  May never work since poolfs is x86-64 and nodes are 32-bit.
if False and len(cmd) > 50:
    shell_cmd = 'ssh -t poolfs /bin/sh -l'
    print "%s: running %s on poolfs" % (progname, cmd[0])
else:
    shell_cmd = 'oarsub -I'
    if options.job_name:
        shell_cmd += ' -n "%s"' % options.job_name
    if options.dest_queue:
        shell_cmd += ' -q ' + options.dest_queue
    shell_cmd += ' -d %s' % cwd

shell = Shell(shell_cmd)

try:
    # chdir to cwd
    (output, status) = shell.do_command('cd ' + cwd)

    if status != 0:
        raise OSError, "Can't chdir to %s" % cwd

    # wacky hack: sometimes scons will create an output directory then
    # fork a job to generate files in that directory, and the job will
    # get run before the directory creation propagates through NFS.
    # This hack looks for a '-o' option indicating an output file and
    # waits for the corresponding directory to appear if necessary.
    try:
        if 'cc' in cmd[0] or 'g++' in cmd[0]:
            output_dir = os.path.dirname(cmd[cmd.index('-o')+1])
        elif 'm5' in cmd[0]:
            output_dir = cmd[cmd.index('-d')+1]
        else:
            output_dir = None
    except (ValueError, IndexError):
        # no big deal if there's no '-o'/'-d' or if it's the final argument
        output_dir = None

    if output_dir:
        secs_waited = 0
        while not shell.dir_exists(output_dir) and secs_waited < 90:
            time.sleep(5)
            secs_waited += 5
        if secs_waited > 30:
            print "waited", secs_waited, "seconds for", output_dir

    # run command
    if options.stdout_file:
        cmd += ['>', options.stdout_file]
    if options.stderr_file:
        cmd += ['2>', options.stderr_file]
    try:
        (output, status) = shell.do_command(' '.join(cmd), options.cmd_timeout)
    except pexpect.TIMEOUT:
            print >>sys.stderr, "%s: command timed out after %d seconds." \
                  % (progname, options.cmd_timeout)
            shell.sendline('~.') # oarsub/ssh termination escape sequence
            shell.safe_close()
            status = 3
    if output:
        print output
finally:
    # end job
    if shell.isalive():
        shell.sendline('exit')
        shell.expect('Disconnected from OAR job .*')
        shell.safe_close()

    # if there was an error, log the output even if not requested
    if status != 0 or options.save_log:
        log = file('qdo-log.' + str(os.getpid()), 'w')
        log.write(shell.full_output)
        log.close()
del shell

sys.exit(status)
